package ru.sgu.codecenter.fidx.model;

import ru.sgu.codecenter.fidx.Configuration;
import ru.sgu.codecenter.fidx.OperationFailedException;
import org.apache.lucene.util.Version;
import org.apache.lucene.queryParser.*;
import org.apache.lucene.analysis.*;
import org.apache.lucene.document.*;
import org.apache.lucene.search.*;
import org.apache.lucene.index.*;
import org.apache.lucene.store.*;

import java.io.IOException;
import java.util.*;
import java.io.File;

public class LuceneEngine implements Engine {
    private PerFieldAnalyzerWrapper analyzer;
    private final Directory storageDirectory;
    private IndexWriter indexWriter;

    public LuceneEngine(File storageDir) {
        try {
            this.storageDirectory = new SimpleFSDirectory(storageDir);
        } catch (IOException e) {
            throw new OperationFailedException("Unable to access the index storage directory [file=\"" + storageDir + "\"].", e);
        }

        Map<String, Analyzer> analyzers = new TreeMap<String, Analyzer>();
        analyzers.put("path", new KeywordAnalyzer());
        analyzers.put("name", new KeywordAnalyzer());

        analyzer = new PerFieldAnalyzerWrapper(new WhitespaceAnalyzer(Version.LUCENE_36), analyzers);
    }

    synchronized IndexWriter getIndexWriter() {
        if (indexWriter == null) {
            try {
                indexWriter = new IndexWriter(storageDirectory, new IndexWriterConfig(Version.LUCENE_36, analyzer));
            } catch (IOException e) {
                indexWriter = null;
            }
        }

        return indexWriter;
    }

    List<FileDocument> executeQuery(Query query) {
        try {
            IndexReader reader = IndexReader.open(storageDirectory);
            IndexSearcher searcher = new IndexSearcher(reader);
            TopScoreDocCollector collector = TopScoreDocCollector.create(Configuration.getHitsCount() + 1, true);
            searcher.search(query, collector);
            List<FileDocument> result = constructFileDocuments(searcher, collector.topDocs().scoreDocs);
            reader.close();
            return result;
        } catch (IOException e) {
            throw new OperationFailedException("Unable to execute query \"" + query + "\".", e);
        }
    }

    @Override
    public List<FileDocument> findByFilePathPrefix(String pathPrefix) {
        pathPrefix = preprocessPathOrName(pathPrefix);
        Term term = new Term("path", pathPrefix);
        Query query = new PrefixQuery(term);
        return executeQuery(query);
    }

    @Override
    public List<FileDocument> findByFilePath(String path) {
        try {
            path = preprocessPathOrName(path);
            Query query = new QueryParser(Version.LUCENE_36, "path", analyzer).parse("\"" + path + "\"");
            return executeQuery(query);
        } catch (ParseException e) {
            throw new OperationFailedException("Error while parsing with query \"" + path + "\".", e);
        }
    }

    @Override
    public List<FileDocument> findByFileName(String name) {
        try {
            name = preprocessPathOrName(name);
            Query query = new QueryParser(Version.LUCENE_36, "name", analyzer).parse("\"" + name + "\"");
            return executeQuery(query);
        } catch (ParseException e) {
            throw new OperationFailedException("Error while parsing with query \"" + name + "\".", e);
        }
    }

    private String preprocessPathOrName(String name) {
        return name.replaceAll("\\\\", "/").replaceAll(":", "/").toLowerCase();
    }

    @Override
    public List<FileDocument> findByContentSubstring(String substring) {
        try {
            Query query = new QueryParser(Version.LUCENE_36, "content", analyzer).parse("\"" + substring + "\"");
            return executeQuery(query);
        } catch (ParseException e) {
            throw new OperationFailedException("Error while parsing with query \"" + substring + "\".", e);
        }
    }

    @Override
    public List<FileDocument> findByContentQuery(String queryValue) {
        try {
            Query query = new QueryParser(Version.LUCENE_36, "content", analyzer).parse(queryValue);
            return executeQuery(query);
        } catch (ParseException e) {
            throw new OperationFailedException("Error while parsing with query \"" + queryValue + "\".", e);
        }
    }

    @Override
    public List<FileDocument> findBySha1(String sha1) {
        try {
            Query query = new QueryParser(Version.LUCENE_36, "sha1", analyzer).parse(sha1);
            return executeQuery(query);
        } catch (ParseException e) {
            throw new OperationFailedException("Error while parsing with sha1 \"" + sha1 + "\".", e);
        }
    }

    @Override
    public void insert(FileDocument fileDocument) {
        Document document = new Document();
        String path = preprocessPathOrName(fileDocument.getPath());
        String name = preprocessPathOrName(fileDocument.getName());

        document.add(new Field("path", path, Field.Store.YES, Field.Index.ANALYZED));
        document.add(new Field("name", name, Field.Store.YES, Field.Index.ANALYZED));
        document.add(new Field("content", fileDocument.getContent(), Field.Store.YES, Field.Index.ANALYZED));
        document.add(new Field("rawPath", fileDocument.getPath(), Field.Store.YES, Field.Index.NOT_ANALYZED));
        document.add(new Field("rawName", fileDocument.getName(), Field.Store.YES, Field.Index.NOT_ANALYZED));
        document.add(new Field("sha1", fileDocument.getSha1(), Field.Store.YES, Field.Index.ANALYZED));

        try {
            getIndexWriter().addDocument(document);
            getIndexWriter().commit();
        } catch (IOException e) {
            throw new OperationFailedException("Unable to add a document to the index.", e);
        }
    }

    @Override
    public void delete(FileDocument fileDocument) {
        try {
            getIndexWriter().deleteDocuments(new Term("path", preprocessPathOrName(fileDocument.getPath())));
            getIndexWriter().commit();
        } catch (IOException e) {
            throw new OperationFailedException("Unable to delete a document from the index.", e);
        }
    }

    private List<FileDocument> constructFileDocuments(IndexSearcher searcher, ScoreDoc[] scoreDocs) throws IOException {
        List<FileDocument> result = new ArrayList<FileDocument>();

        for (ScoreDoc scoreDoc : scoreDocs) {
            int docId = scoreDoc.doc;
            Document document = searcher.doc(docId);

            FileDocument fileDocument = new FileDocument();
            fileDocument.setPath(document.get("rawPath"));
            fileDocument.setName(document.get("rawName"));
            fileDocument.setContent(document.get("content"));

            result.add(fileDocument);
        }

        Collections.sort(result);
        return result;
    }

    @Override
    public void close() {
        try {
            getIndexWriter().close();
        } catch (Exception e) {
            // no operations.
        }
    }
}
